package com.example.demo.encrypt;

import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Security;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;

/**
 * @author tangjunmao
 * @date 2018/10/22
 */
public class AESUtils {

    /**
     * 加密方式
     */
    private static final String KEY_ALGORITHM = "AES";
    /**
     * 安全随机签名算法
     */
    private static final String SECURE_ALGORITHM = "SHA1PRNG";
    /**
     * 加密算法
     */
    private static final String ENCRYPT_ALGORITHM = "AES/ECB/PKCS5Padding";
    /**
     * PBKDF2WithHmacSHA1安全随机签名算法
     */
    private static final String SECURE_ALGORITHM_PBKDF2 = "PBKDF2WithHmacSHA1";
    /**
     * 加密算法CBC
     */
    private static final String ENCRYPT_ALGORITHM_CBC = "AES/CBC/PKCS5Padding";


    private static final int KEY_LENGTH = 128;

    private static final int H5_KEY_LENGTH = 16;

    private static final BouncyCastleProvider bouncyCastleProvider = new BouncyCastleProvider();

    /**
     * 生成随机密钥，一次一密
     */
    public static String randomKey() {
        long randomKey = ThreadLocalRandom.current().nextLong();
        return String.valueOf(randomKey);
    }

    /**
     * 报文加密
     *
     * @param randomKey 随机密钥
     * @param str       上送报文
     * @return 报文密文
     */
    public static String encrypt(String str, String randomKey)
        throws Exception {
        byte[] aesData = doCipher(randomKey, str.getBytes(StandardCharsets.UTF_8), Cipher.ENCRYPT_MODE);
        return Base64.encodeBase64String(aesData);
    }

    /**
     * 对报文进行解密
     *
     * @param str       aes 加密后的报文
     * @param randomKey aes秘钥
     */
    public static String decrypt(String str, String randomKey)
        throws Exception {
        byte[] plain = doCipher(randomKey, Base64.decodeBase64(str), Cipher.DECRYPT_MODE);
        return new String(plain, StandardCharsets.UTF_8);
    }

    /**
     * 基于aes的加解密公共方法
     * TODO 考虑用单例实现SecureRandom，KeyGenerator，SecretKey，SecretKeySpec，Cipher
     *
     * @param randomKey aes key
     */
    private static byte[] doCipher(String randomKey, byte[] bytes, int mode) throws Exception {
        SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
        secureRandom.setSeed(randomKey.getBytes(StandardCharsets.UTF_8));
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(128, secureRandom);
        SecretKey secretKey = keyGenerator.generateKey();
        SecretKeySpec keySpec = new SecretKeySpec(secretKey.getEncoded(), "AES");
        Security.addProvider(bouncyCastleProvider);
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(mode, keySpec);
        return cipher.doFinal(bytes);
    }



    /**
     * aes-cbc模式加密在加密和解密是需要一个初始化向量(Initialization Vector, IV)，
     * 在每次加密之前或者解密之后，使用初始化向量与明文或密文异或。
     * @param randomKey 随机密钥
     * @param plainData 上送报文
     */
    public static String encryptWithCBC(String plainData, String randomKey) throws Exception {
        SecretKeySpec skeySpec = secretKeyInitWithPBKDF2(randomKey);
        Security.addProvider(bouncyCastleProvider);
        Cipher cipher = Cipher.getInstance(ENCRYPT_ALGORITHM_CBC);
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec, new IvParameterSpec(getIV(randomKey)));
        byte[] aesData = cipher.doFinal(plainData.getBytes(Charset.forName("utf-8")));
        return Base64.encodeBase64String(aesData);
    }

    /**
     * 报文解密
     * AES算法为AES/CBC/PKCS5Padding，对报文进行aes对称加密
     * @param aesBase64 Base64格式的加密报文
     * @param randomKey 解密密钥
     */
    public static String decryptWithCBC(String aesBase64, String randomKey) throws Exception {
        byte[] aesData = Base64.decodeBase64(aesBase64);
        Security.addProvider(bouncyCastleProvider);
        Cipher aesCipher = Cipher.getInstance(ENCRYPT_ALGORITHM_CBC);;
        SecretKeySpec keySpec = secretKeyInitWithPBKDF2(randomKey);
        aesCipher.init(Cipher.DECRYPT_MODE, keySpec,new IvParameterSpec(getIV(randomKey)));
        byte[] plain = aesCipher.doFinal(aesData);
        return new String(plain, Charset.forName("utf-8"));
    }
    public static byte[] getIV(String randomKey){
        byte[] iv = new byte[H5_KEY_LENGTH];
        if(randomKey.length() < H5_KEY_LENGTH) {
            randomKey = addZeroForNum(randomKey,H5_KEY_LENGTH);
        }
        iv = Arrays.copyOf(randomKey.getBytes(),H5_KEY_LENGTH);
        return iv;
    }
    /**
     * 构造密钥
     *
     * @param randomKey 随机密钥串
     * @return SecretKeySpec 密钥对象
     */
    private static SecretKeySpec secretKeyInitWithPBKDF2(String randomKey)
            throws NoSuchAlgorithmException, InvalidKeySpecException {
        int iterations = 1000;
        char[] chars = randomKey.toCharArray();
        byte[] salt =String.valueOf(randomKey.hashCode()).getBytes(Charset.forName("utf-8"));
        PBEKeySpec spec = new PBEKeySpec(chars,salt,iterations,KEY_LENGTH);
        SecretKeyFactory skf = SecretKeyFactory.getInstance(SECURE_ALGORITHM_PBKDF2);
        return new SecretKeySpec(skf.generateSecret(spec).getEncoded(), KEY_ALGORITHM);
    }

    /**
     * 不够长度右补0
     * @param str
     * @param strLength
     */
    public static String addZeroForNum(String str, int strLength) {
        int strLen = str.length();
        if (strLen < strLength) {
            while (strLen < strLength) {
                StringBuffer sb = new StringBuffer();
                sb.append(str).append("0");//右补0
                str = sb.toString();
                strLen = str.length();
            }
        }

        return str;
    }

}
